Closure
闭包在Swift中非常有用。通俗的解释就是一个 Int 类型里存储着一个整数,一个 String 类型包含着一串字符,同样,闭包是一个包含着函数的类型。有了闭包,你就可以处理很多在一些古老的语言中不能处理的事情。这是因为闭包使用的多样性,比如你可以将闭包赋值给一个变量,你也可以将闭包作为一个函数的参数,你甚至可以将闭包作为一个函数的返回值。它的强大之处可见一斑。
在Swift的很多文档教材中都说函数是“一等公民”,起初我还不是很理解“一等公民”是什么意思,但当我理解了闭包以及它的强大功能后,我恍然大悟、茅塞顿开、醍醐灌顶。原来闭包的这些特性就是“一等公民”的特性啊!参见维基百科 First-class citizen 。
Swift中的闭包类似Objective-C中的Block。其实,如果你想在Swift中实现Objective-C里的Block功能,你可以直接使用闭包来代替。Block和闭包的区别只是语法的不同而已,而且闭包的可读性比较强。
函数是闭包吗?
虽然你还没有意识到,但我们确实已经在Swift中这么用了。Swift中的函数就是闭包,在Apple的官方文档中有这样的描述:
闭包有三种形式:
全局函数是一个有名字但不会捕获任何值的闭包。
嵌套函数是一个有名字并可以捕获到其封闭函数域内的值的闭包。
闭包表达式是一个利用轻量级语法所写的,可以捕获其上下文中变量或常量值的匿名闭包。
今天,我们要讨论的是第三种形式,尤其讨论它是如何将繁复的、可读性比较差的业务逻辑代码压缩成高可读性、简明明了的形式。
大家还记得数组的 map 方法么?它的参数就是一个闭包,它会将数组里的每一个元素放在闭包中进行处理,然后返回一个新的数组,甚至是与原数组不同元素类型的新数组。
map 函数的原型如下:
func map<U>(transform: (T) -> U) -> [U]
我们可以看到该函数使用了泛型。 (T) -> U 是一个泛型闭包,它的意思就是类型 T 将会在闭包中进行逻辑处理,然后返回 U 类型。最后 map 函数会返回一个 U 类型的数组。
用一个例子来说明。今天我办生日聚会,要迎接很多人,并且为每个人都准备了一句欢迎词。我们要怎么做呢?首先我们将迎接的人放进一个数组名叫 guestList ,然后用一个名叫 greetPeople 的函数为每个人生成欢迎词:
func greetPeople(person: String) -> String
{
return "Hello, \(person)!"
}
let guestList = ["Chris", "Jill", "Tim"]
let fullGreetings = guestList.map(greetPeople)
然后将 greetPeople 函数作为 guestList 数组的 map 函数的参数传入,并返回一个新的数组 fullGreetings ,这个数组就包含了每个人的欢迎词。
如果我们想展示一下每个人的欢迎词,我们甚至可以这样写:
fullGreetings.map(println)
这时也许有人要质疑了, println 函数不是没有返回值么?那么 map 函数会返回什么呢?其实每一个没有返回值的函数,都会返回一个空的元组( tuple ),所以说上述代码的返回值其实是 Array<()> 。
上面的例子中我们就是将一个全局函数 greetPeople 作为一个闭包来使用的。
简明扼要的闭包表达式
其实Swift已经为我们提供了很多简化的语法,可以让我们保证代码的高可读性和维护性。还用上面的例子来说明,对于 greetPeople 这个全局函数来说,其实只需要使用一次,所以我们没必要单独定义这个函数。我们可以直接使用闭包表达式来处理:
let fullGreetings = guestList.map({(person: String) -> String in return "Hello, \(person)!"})
闭包表达式其实是函数的字面值,官方一般称之为匿名函数。一般当我们需要使用函数快速的实现一个简短的处理逻辑并且只使用一次的时候,我们可以省去函数名,使用简化的语法。上面的代码中可以看到关键字 in 之前是闭包表达式的参数和返回值, in 之后是闭包表达式实际处理逻辑的代码区域。
下面我们将使用Swift更多的特性来进一步简化闭包表达式。
我们知道Swift中有类型推断的特性,所以我们可以取掉参数类型:
let fullGreetings = guestList.map({(person) -> String in return "Hello, \(person)!" })
像我们示例中的这种单一闭包表达式,编译器可以根据 in 之前的返回值类型和 return 之后的返回数据类型自动判断,所以我们可以省略返回值和 return 关键字:
let fullGreetings = guestList.map({person in "Hello, \(person)!" })
其实在Swift中还提供了参数的简写方式: $0 代表第一个参数、 $1 代表第二个参数以此类推。所以我们又可以将参数名称省略:
let fullGreetings = guestList.map({ "Hello, \($0)!" })
当函数的最后一个参数是闭包时,可以将闭包写在 () 之外,这也是Swift的一个特性,所以我们还可以继续简化:
let fullGreetings = guestList.map(){ "Hello, \($0)!" }
当函数有且仅有一个参数,并该参数是闭包时,不但可以将闭包写在 () 外,还可以省略 () :
let fullGreetings = guestList.map{ "Hello, \($0)!" }
到目前为止,示例中的闭包表达式已经被我们根据Swift的特性,简化为简明扼要、高可读性的闭包表达式了,是不是很酷呢!
非逃逸闭包与逃逸闭包(Nonescaping Closures vs Escaping Closures)
当闭包作为函数的参数传入时,很有可能这个闭包在函数返回之后才会被执行,这就是逃逸闭包。
在Swift中可以在参数名前标注 @noescape 来指明这个闭包是不允许逃逸出这个函数的。因为非逃逸闭包只能在函数体中被执行,不能脱离函数体执行,所以这使得编译器可以明确的知道运行时的上下文环境,进而做出优化。
比如, sort(_:) 方法可以接受一个用于元素比较的闭包参数,它被指明为 @noescape ,因为排序结束后这个闭包就没用了。
一般情况下,一些异步函数会使用逃逸闭包。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。比如网络请求中处理服务器返回请求的闭包。在这种情况下,逃逸闭包就可以派上用场了。一个常见的例子如下:
varhandlers: [()->Void] = []
func functionWithEscapingClosure(handler: () -> Void){
truehandlers.append(handler)
}
因为编译器知晓非逃逸闭包的上下文环境,所以非逃逸闭包中可以不写 self 。
比如:
classA {
truevarx
truefunc doSometing{
truetruefunctionWithEscapingClosure{x=250}
true}
}
自动闭包
自动闭包,顾名思义是一种自动创建的闭包,用于包装函数参数的表达式,可以说是一种简便语法。
自动闭包不接受任何参数,被调用时会返回被包装在其中的表达式的值。
自动闭包的好处之二是让你能够延迟求值,因为代码段不会被执行直到你调用这个闭包,这样你就可以控制代码什么时候执行。
var students =["A","B","C"]
letstudentsProvider ={ students.removeAtIndex(0) }
studentsProvider()
尽管调用了 removeAtIndex() 方法,但是此时并不会执行,知道第三行调用了这个闭包方法。
这里注意 studentsProvider 与 studentsProvider() 二者的不同,前者的类型是 ()->String 指向了一个函数,后者的类型是 String 。
@autoclosure
当闭包作为函数参数时,可以将参数标记 @autoclosure 来接收自动闭包。 @autoclosure 暗含了非逃逸闭包的特性,如果你想让这个自动闭包具有逃逸的特性需要更改标记为 @autoclosure(escaping) 。
1.1 用途
闭包在很多语言中都有应用,它在OC中被叫做 Blocks ,在Java中被叫做 Lambda表达式 ,也有直接叫做匿名函数的。
简单的说闭包就是一种带有局部变量的匿名函数。
在C语言中,函数可以通过函数名直接调用,也可以通过函数指针调用,但是这都需要开发者知道函数的名字(函数指针也需要知道函数名以便在被赋值时得到函数的地址)。
可能你会问,为什么要用闭包呢?一个常见的例子如下:
实现按钮的回调方法。
intbuttonId =0;
voidbuttonCallBack(intevent){
NSLog(@"id = %d,event = %d",buttonId,event);
}
现在把情况扩展到多个按钮,如下:
voidbuttonCallBack(intbuttonId,intevent){
NSLog(@"id = %d,event = %d",buttonId,event);
}
voidsetButtonCallbacks(){//工厂方法
for(inti =0; i < MAX; i++) {
buttonId = i;
setButtonCallBack(i,&buttonCallBack); //省略单个set的方法了,只为说明思路
}
}
显然回调方法保存了按钮的ID以及回调函数的指针。闭包的出现可以使代码更加简洁,可以直接将回调卸载函数内,而不用再去写回调函数,例子如下:
voidsetButtonCallbacks(){
for(inti =0; i < MAX; i++) {
setButtonCallbackUsingBlock(i,^(intevent){
NSLog(@"id = %d,event = %d",buttonId,event);
});
}
}
注意:当用于函数参数时,Block 应该放在参数列表的最后一个。
下面介绍 Blocks 的语法:
1.2 语法
Blocks 的语法有些晦涩,以至于有 fuckingblocksyntax 这个网站专门记录语法。
如下是 Blocks 的语法:
^ 返回值类型 参数列表 表达式
比如:
^int(intcount){returncount+1;}
Blocks 是可以进行缩写的,如下
1.2.1 省略返回值类型
当省略返回值类型时,如果反表达式中又 return 语句就使用该返回值的类型,如果表达式没有 return 语句就是 void 类型。
如果有多个 return 语句,那么其类型必须相同。省略返回值类型后,例子如下:
^(intcount){returncount+1};
1.2.2 省略参数参数列表
如果不使用参数,参数列表也可以省略,例子如下:
^void(void) {NSLog(@"helloworld");}
可以省略 返回值类型 与 参数列表 缩写为如下的形式:
^{NSLog(@"helloworld");}
1.2.2 Block 类型
与C语言中的变量相同, Block 类型的变量可以作一下用途。
局部变量
函数参数
静态变量
静态全局变量
全局变量
如下是一个常见的声明 Block 类型的变量的例子:
int(^blk) (int) = ^(intcount){returncount+1;}
当然,在函数参数中使用 Block 类型的变量就可以向函数传递 Block ,在函数返回值中指定 Block 类型,可以将 Block 作为函数的返回值返回。分别对应如下的两个例子:
void function(int(^blk)(int))
int (^func())(int) {
return^(intcount){returncount+1;}
}
到这里, Block 的语法变得着实复杂了,可以通过 typedef 做简化。如下是简化的例子:
typedef int (^blk_t) (int);
//原来的写法
void func(int (^blk)(int))
//新的写法
void func(blk_t blk)
//原来的写法
int (^func()(int))
//新的写法
blk_t func()
1.2.3 捕获外部变量
Block 中捕获外部的局部变量具有瞬间性,即如果变量被 Block 捕获后修改了值,那么 Block 中捕获的变量的值并不会改变。
此外, Block 无法给捕获的外部变量赋值。
1.2.4 __block修饰符
Block 捕获外部的局部变量后,无法改变它的值,使用附有 __block 修饰符的局部变量可以在 Block 中赋值。
1.2.5 注意事项
Block 中虽然无法给捕获的局部变量赋值,但是对于OC的对象的一些方法,是可以执行的,比如捕获一个NSMutableArray后,执行 addObject 方法。这不会有任何问题,因为这相当于捕获了对象的实例指针。
对于C语言中的数组, Block 中并没有实现对之的捕获方法。可以使用指针来解决这个问题。
char*text ="helloworld";
void(^blk)(void) = ^{
NSLog(@"%c",text[2]);
}
在 OC 中,Block 有如下三种类型:
_NSConcreteGlobalBlock
_NSConcreteStackBlock
_NSConcreteMallocBlock
如下是具体的描述
_NSConcreteGlobalBlock 是全局的静态block,不会访问任何外部变量。这种不捕捉外界变量的block是不需要内存管理的,这种block不存在于Heap或是Stack而是作为代码片段存在,类似于C函数。
_NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。
在 MRC 下 block 在创建时是 stack 对象,如果我们需要在离开当前函数仍能够使用我们创建的 block 。我们就需要把它拷贝到堆上以便进行以引用计数为基础的内存管理。
ARC做了什么?
文档里是这么说的:
With the exception of retains done as part of initializing a strong parameter variable or reading a weak variable, whenever these semantics call for retaining a value of block-pointer type, it has the effect of a Block_copy. The optimizer may remove such copies when it sees that the result is used only as an argument to a call.
即在 ARC 下创建的 block 仍然是 _NSConcreteStackBlock 类型,当 block 被引用或返回时,ARC 帮助我们完成了 copy 和内存管理的工作。这种 block 变成了 _NSConcreteMallocBlock 类型。